package com.twelve.all.nfc;

import android.content.Context;
import android.net.Uri;
import android.nfc.Tag;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.NfcA;
import android.os.Environment;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.widget.Toast;

import com.twelve.all.R;
import com.twelve.all.base.NFCApp;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Locale;

import static com.twelve.all.base.NFCApp.mAppContext;


/**
 * Created by 张龙臣 on 2020/8/29.
 */

public class Utils {
    private static String LOG_TAG = "NFC Test";

    public static String byte2Hex(byte[] bytes) {
        StringBuilder ret = new StringBuilder();
        if (bytes != null) {
            for (Byte b : bytes) {
                ret.append(String.format("%02X", b.intValue() & 0xFF));
            }
        }
        return ret.toString();
    }

    /**
     * Convert a string of hex data into a byte array.
     * Original author is: Dave L. (http://stackoverflow.com/a/140861).
     *
     * @param hex The hex string to convert
     * @return An array of bytes with the values of the string.
     */
    public static byte[] hex2ByteArray(String hex) {
        if (!(hex != null && hex.length() % 2 == 0
                && hex.matches("[0-9A-Fa-f]+"))) {
            return null;
        }
        int len = hex.length();
        byte[] data = new byte[len / 2];
        try {
            for (int i = 0; i < len; i += 2) {
                data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                        + Character.digit(hex.charAt(i + 1), 16));
            }
        } catch (Exception e) {
            Log.d(LOG_TAG, "Argument(s) for hexStringToByteArray(String s)"
                    + "was not a hex string");
        }
        return data;
    }

    /**
     * Convert a hex string to ASCII string.
     *
     * @param hex Hex string to convert.
     * @return Converted ASCII string. Null on error.
     */
    public static String hex2Ascii(String hex) {
        if (!(hex != null && hex.length() % 2 == 0
                && hex.matches("[0-9A-Fa-f]+"))) {
            return null;
        }
        byte[] bytes = hex2ByteArray(hex);
        String ret = null;
        // Replace non printable ASCII with ".".
        for (int i = 0; i < bytes.length; i++) {
            if (bytes[i] < (byte) 0x20 || bytes[i] == (byte) 0x7F) {
                bytes[i] = (byte) 0x2E;
            }
        }
        // Hex to ASCII.
        try {
            ret = new String(bytes, "US-ASCII");
        } catch (UnsupportedEncodingException e) {
            Log.e(LOG_TAG, "Error while encoding to ASCII", e);
        }
        return ret;
    }

    /**
     * Convert a ASCII string to a hex string.
     *
     * @param ascii ASCII string to convert.
     * @return Converted hex string.
     */
    public static String ascii2Hex(String ascii) {
        if (!(ascii != null && !ascii.equals(""))) {
            return null;
        }
        char[] chars = ascii.toCharArray();
        StringBuilder hex = new StringBuilder();
        for (int i = 0; i < chars.length; i++) {
            hex.append(String.format("%02X", (int) chars[i]));
        }
        return hex.toString();
    }

    /**
     * Convert a hex string to a binary string (with leading zeros).
     *
     * @param hex Hex string to convert.
     * @return Converted binary string.
     */
    public static String hex2Bin(String hex) {
        if (!(hex != null && hex.length() % 2 == 0
                && hex.matches("[0-9A-Fa-f]+"))) {
            return null;
        }
        String bin = new BigInteger(hex, 16).toString(2);
        // Pad left with zeros (have not found a better way...).
        if (bin.length() < hex.length() * 4) {
            int diff = hex.length() * 4 - bin.length();
            StringBuilder pad = new StringBuilder();
            for (int i = 0; i < diff; i++) {
                pad.append("0");
            }
            pad.append(bin);
            bin = pad.toString();
        }
        return bin;
    }

    public static String bin2Hex(String bin) {
        if (!(bin != null && bin.length() % 8 == 0
                && bin.matches("[0-1]+"))) {
            return null;
        }
        String hex = new BigInteger(bin, 2).toString(16);
        if (hex.length() % 2 != 0) {
            hex = "0" + hex;
        }
        return hex;
    }
    /**
     * Convert the Access Condition bytes to a matrix containing the
     * resolved C1, C2 and C3 for each block.
     * @param acBytes The Access Condition bytes (3 byte).
     * @return Matrix of access conditions bits (C1-C3) where the first
     * dimension is the "C" parameter (C1-C3, Index 0-2) and the second
     * dimension is the block number (Index 0-3). If the ACs are incorrect
     * null will be returned.
     */
    public static byte[][] acBytesToACMatrix(byte acBytes[]) {
        // ACs correct?
        // C1 (Byte 7, 4-7) == ~C1 (Byte 6, 0-3) and
        // C2 (Byte 8, 0-3) == ~C2 (Byte 6, 4-7) and
        // C3 (Byte 8, 4-7) == ~C3 (Byte 7, 0-3)
        byte[][] acMatrix = new byte[3][4];
        if (acBytes.length > 2 &&
                (byte)((acBytes[1]>>>4)&0x0F)  ==
                        (byte)((acBytes[0]^0xFF)&0x0F) &&
                (byte)(acBytes[2]&0x0F) ==
                        (byte)(((acBytes[0]^0xFF)>>>4)&0x0F) &&
                (byte)((acBytes[2]>>>4)&0x0F)  ==
                        (byte)((acBytes[1]^0xFF)&0x0F)) {
            // C1, Block 0-3
            for (int i = 0; i < 4; i++) {
                acMatrix[0][i] = (byte)((acBytes[1]>>>4+i)&0x01);
            }
            // C2, Block 0-3
            for (int i = 0; i < 4; i++) {
                acMatrix[1][i] = (byte)((acBytes[2]>>>i)&0x01);
            }
            // C3, Block 0-3
            for (int i = 0; i < 4; i++) {
                acMatrix[2][i] = (byte)((acBytes[2]>>>4+i)&0x01);
            }
            return acMatrix;
        }
        return null;
    }

    /**
     * Check if key B is readable.
     * Key B is readable for the following configurations:
     * <ul>
     * <li>C1 = 0, C2 = 0, C3 = 0</li>
     * <li>C1 = 0, C2 = 0, C3 = 1</li>
     * <li>C1 = 0, C2 = 1, C3 = 0</li>
     * </ul>
     * @param c1 Access Condition byte "C1"
     * @param c2 Access Condition byte "C2"
     * @param c3 Access Condition byte "C3"
     * @return True if key B is readable. False otherwise.
     */
    public static boolean isKeyBReadable(byte c1, byte c2, byte c3) {
        return c1 == 0
                && ((c2 == 0 && c3 == 0)
                || (c2 == 1 && c3 == 0)
                || (c2 == 0 && c3 == 1));
    }

    /**
     * Depending on the provided Access Conditions this method will return
     * with which key you can achieve the operation ({@link Operations})
     * you asked for.<br />
     * This method contains the table from the NXP MIFARE Classic Datasheet.
     * @param c1 Access Condition byte "C!".
     * @param c2 Access Condition byte "C2".
     * @param c3 Access Condition byte "C3".
     * @param op The operation you want to do.
     * @param isSectorTrailer True if it is a Sector Trailer, False otherwise.
     * @param isKeyBReadable True if key B is readable, False otherwise.
     * @return The operation "op" is possible with:<br />
     * <ul>
     * <li>0 - Never.</li>
     * <li>1 - Key A.</li>
     * <li>2 - Key B.</li>
     * <li>3 - Key A or B.</li>
     * <li>-1 - Error.</li>
     * </ul>
     */
    public static int getOperationInfoForBlock(byte c1, byte c2, byte c3,
                                               Operations op, boolean isSectorTrailer, boolean isKeyBReadable) {
        // Is Sector Trailer?
        if (isSectorTrailer) {
            // Sector Trailer.
            if (op != Operations.ReadKeyA && op != Operations.ReadKeyB
                    && op != Operations.ReadAC
                    && op != Operations.WriteKeyA
                    && op != Operations.WriteKeyB
                    && op != Operations.WriteAC) {
                // Error. Sector Trailer but no Sector Trailer permissions.
                return 4;
            }
            if          (c1 == 0 && c2 == 0 && c3 == 0) {
                if (op == Operations.WriteKeyA
                        || op == Operations.WriteKeyB
                        || op == Operations.ReadKeyB
                        || op == Operations.ReadAC) {
                    return 1;
                }
                return 0;
            } else if   (c1 == 0 && c2 == 1 && c3 == 0) {
                if (op == Operations.ReadKeyB
                        || op == Operations.ReadAC) {
                    return 1;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 0 && c3 == 0) {
                if (op == Operations.WriteKeyA
                        || op == Operations.WriteKeyB) {
                    return 2;
                }
                if (op == Operations.ReadAC) {
                    return 3;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 1 && c3 == 0) {
                if (op == Operations.ReadAC) {
                    return 3;
                }
                return 0;
            } else if   (c1 == 0 && c2 == 0 && c3 == 1) {
                if (op == Operations.ReadKeyA) {
                    return 0;
                }
                return 1;
            } else if   (c1 == 0 && c2 == 1 && c3 == 1) {
                if (op == Operations.ReadAC) {
                    return 3;
                }
                if (op == Operations.ReadKeyA
                        || op == Operations.ReadKeyB) {
                    return 0;
                }
                return 2;
            } else if   (c1 == 1 && c2 == 0 && c3 == 1) {
                if (op == Operations.ReadAC) {
                    return 3;
                }
                if (op == Operations.WriteAC) {
                    return 2;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 1 && c3 == 1) {
                if (op == Operations.ReadAC) {
                    return 3;
                }
                return 0;
            } else {
                return -1;
            }
        } else {
            // Data Block.
            if (op != Operations.Read && op != Operations.Write
                    && op != Operations.Increment
                    && op != Operations.DecTransRest) {
                // Error. Data block but no data block permissions.
                return -1;
            }
            if          (c1 == 0 && c2 == 0 && c3 == 0) {
                return (isKeyBReadable) ? 1 : 3;
            } else if   (c1 == 0 && c2 == 1 && c3 == 0) {
                if (op == Operations.Read) {
                    return (isKeyBReadable) ? 1 : 3;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 0 && c3 == 0) {
                if (op == Operations.Read) {
                    return (isKeyBReadable) ? 1 : 3;
                }
                if (op == Operations.Write) {
                    return 2;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 1 && c3 == 0) {
                if (op == Operations.Read
                        || op == Operations.DecTransRest) {
                    return (isKeyBReadable) ? 1 : 3;
                }
                return 2;
            } else if   (c1 == 0 && c2 == 0 && c3 == 1) {
                if (op == Operations.Read
                        || op == Operations.DecTransRest) {
                    return (isKeyBReadable) ? 1 : 3;
                }
                return 0;
            } else if   (c1 == 0 && c2 == 1 && c3 == 1) {
                if (op == Operations.Read || op == Operations.Write) {
                    return 2;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 0 && c3 == 1) {
                if (op == Operations.Read) {
                    return 2;
                }
                return 0;
            } else if   (c1 == 1 && c2 == 1 && c3 == 1) {
                return 0;
            } else {
                // Error.
                return -1;
            }
        }
    }

    /**
     * Read a file line by line. The file should be a simple text file.
     * Empty lines will not be read.
     * @param file The file to read.
     * @param readComments Whether to read comments or to ignore them.
     * Comments are lines STARTING with "#" (and empty lines).
     * @param context  The context in which the possible "Out of memory"-Toast
     * will be shown.
     * @return Array of strings representing the lines of the file.
     * If the file is empty or an error occurs "null" will be returned.
     */
    public static String[] readFileLineByLine(File file, boolean readComments,
                                              Context context) {
        String[] ret = null;
        BufferedReader reader = null;
        if (file != null && isExternalStorageMounted() && file.exists()) {
            try {
                reader = new BufferedReader(new FileReader(file));
                ret = readLineByLine(reader, readComments, context);
            } catch (FileNotFoundException ex) {
                ret = null;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        Log.e(LOG_TAG, "Error while closing file.", e);
                        ret = null;
                    }
                }
            }
        }
        return ret;
    }

    /**
     * Checks if external storage is available for read and write.
     * @return True if external storage is writable. False otherwise.
     */
    public static boolean isExternalStorageMounted() {
        return Environment.MEDIA_MOUNTED.equals(
                Environment.getExternalStorageState());
    }

    /**
     * Read the URI source line by line.
     * @param uri The URI to read from.
     * @param context The context for the content resolver and in which
     *                error/info Toasts are shown.
     * @return The content of the URI, each line representing an array item
     * or Null in case of an read error.
     * @see #readLineByLine(BufferedReader, boolean, Context)
     */
    public static String[] readUriLineByLine(Uri uri, Context context){
        InputStream contentStream = null;
        String[] ret = null;
        try {
            contentStream = context.getContentResolver().openInputStream(uri);
        } catch (FileNotFoundException ex) {
            return null;
        }

        BufferedReader reader = new BufferedReader(new InputStreamReader(contentStream));
        ret = readLineByLine(reader, true, context);
        try {
            reader.close();
        } catch (IOException e) {
            Log.e(LOG_TAG, "Error while closing file.", e);
            return null;
        }
        return ret;
    }

    /**
     * Read a as BufferedReader line by line with the option to ignore
     * lines starting with #.
     * @param reader The reader object initialized with a file (data).
     * @param readComments True if lines starting with # should be ignored.
     * @param context The Context in which error Toasts will be shown.
     * @return The content with each line representing an array item
     * or Null in case of an read error.
     */
    private static String[] readLineByLine(BufferedReader reader,
                                           boolean readComments, Context context) {
        String[] ret = null;
        String line;
        ArrayList<String> linesArray = new ArrayList<>();
        try {
            while ((line = reader.readLine()) != null) {
                // Ignore empty lines.
                // Ignore comments if readComments == false.
                if (!line.equals("")
                        && (readComments || !line.startsWith("#"))) {
                    try {
                        linesArray.add(line);
                    } catch (OutOfMemoryError e) {
                        // Error. File is too big
                        // (too many lines, out of memory).
                        Toast.makeText(context, R.string.info_file_to_big,
                                Toast.LENGTH_LONG).show();
                        return null;
                    }
                }
            }
        } catch (IOException ex) {
            Log.e(LOG_TAG, "Error while reading from file.", ex);
            ret = null;
        }
        if (linesArray.size() > 0) {
            ret = linesArray.toArray(new String[linesArray.size()]);
        } else {
            ret = new String[]{""};
        }
        return ret;
    }

    public static void logUid(String uid) {
        File log = new File(mAppContext.getFilesDir(),
                "/MifareClassicTool" + File.separator + "uid-log-file.txt");
        GregorianCalendar calendar = new GregorianCalendar();
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss",
                Locale.getDefault());
        fmt.setCalendar(calendar);
        String dateFormatted = fmt.format(calendar.getTime());
        String[] logEntry = new String[1];
        logEntry[0] = dateFormatted + ": " + uid;
        saveFile(log, logEntry, true);
    }

    public static boolean saveFile(File file, String[] lines, boolean append) {
        boolean noError = true;
        if (file != null && lines != null && isExternalStorageMounted()) {
            BufferedWriter bw = null;
            try {
                bw = new BufferedWriter(new FileWriter(file, append));
                // Add new line before appending.
                if (append) {
                    bw.newLine();
                }
                int i;
                for(i = 0; i < lines.length-1; i++) {
                    bw.write(lines[i]);
                    bw.newLine();
                }
                bw.write(lines[i]);
            } catch (IOException | NullPointerException ex) {
                Log.e(LOG_TAG, "Error while writing to '"
                        + file.getName() + "' file.", ex);
                noError = false;

            } finally {
                if (bw != null) {
                    try {
                        bw.close();
                    } catch (IOException e) {
                        Log.e(LOG_TAG, "Error while closing file.", e);
                        noError = false;
                    }
                }
            }
        } else {
            noError = false;
        }
        return noError;
    }

    public static String byte2FmtString(byte[] bytes, int fmt) {
        switch(fmt) {
            case 2:
                byte[] revBytes = bytes.clone();
                reverseByteArrayInPlace(revBytes);
                return hex2Dec(byte2Hex(revBytes));
            case 1:
                return hex2Dec(byte2Hex(bytes));
        }
        return byte2Hex(bytes);
    }

    public static void reverseByteArrayInPlace(byte[] array) {
        for(int i = 0; i < array.length / 2; i++) {
            byte temp = array[i];
            array[i] = array[array.length - i - 1];
            array[array.length - i - 1] = temp;
        }
    }

    public static String hex2Dec(String hex) {
        if (!(hex != null && hex.length() % 2 == 0
                && hex.matches("[0-9A-Fa-f]+"))) {
            return null;
        }
        String ret;
        if (hex == null || hex.isEmpty()) {
            ret = "0";
        } else if (hex.length() <= 14) {
            ret = Long.toString(Long.parseLong(hex, 16));
        } else {
            BigInteger bigInteger = new BigInteger(hex , 16);
            ret = bigInteger.toString();
        }
        return ret;
    }

    public static void copyToClipboard(String text, Context context,
                                       boolean showMsg) {
        if (!text.equals("")) {
            android.content.ClipboardManager clipboard =
                    (android.content.ClipboardManager)
                            context.getSystemService(
                                    Context.CLIPBOARD_SERVICE);
            android.content.ClipData clip =
                    android.content.ClipData.newPlainText(
                            "MIFARE classic tool data", text);
            clipboard.setPrimaryClip(clip);
            if (showMsg) {
                Toast.makeText(context, R.string.info_copied_to_clipboard,
                        Toast.LENGTH_SHORT).show();
            }
        }
    }
    public static int checkMifareClassicSupport(Tag tag, Context context) {
        if (tag == null || context == null) {
            // Error.
            return -3;
        }

        if (Arrays.asList(tag.getTechList()).contains(
                MifareClassic.class.getName())) {
            // Device and tag support MIFARE Classic.
            return 0;

            // This is no longer valid. There are some devices (e.g. LG's F60)
            // that have this system feature but no MIFARE Classic support.
            // (The F60 has a Broadcom NFC controller.)
        /*
        } else if (context.getPackageManager().hasSystemFeature(
                "com.nxp.mifare")){
            // Tag does not support MIFARE Classic.
            return -2;
        */

        } else {
            // Check if device does not support MIFARE Classic.
            // For doing so, check if the SAK of the tag indicate that
            // it's a MIFARE Classic tag.
            // See: https://www.nxp.com/docs/en/application-note/AN10834.pdf
            NfcA nfca = NfcA.get(tag);
            byte sak = (byte)nfca.getSak();
            if ((sak>>1 & 1) == 1) {
                // RFU.
                return -2;
            } else {
                if ((sak>>3 & 1) == 1) { // SAK bit 4 = 1?
                    if((sak>>4 & 1) == 1) { // SAK bit 5 = 1?
                        // MIFARE Classic 4k
                        // MIFARE SmartMX 4K
                        // MIFARE PlusS 4K SL1
                        // MIFARE PlusX 4K SL1
                        return -1;
                    } else {
                        if ((sak & 1) == 1) { // SAK bit 1 = 1?
                            // MIFARE Mini
                            return -1;
                        } else {
                            // MIFARE Classic 1k
                            // MIFARE SmartMX 1k
                            // MIFARE PlusS 2K SL1
                            // MIFARE PlusX 2K SL2
                            return -1;
                        }
                    }
                } else {
                    // Some MIFARE tag, but not Classic or Classic compatible.
                    return -2;
                }
            }

            // Old MIFARE Classic support check. No longer valid.
            // Check if the ATQA + SAK of the tag indicate that it's a MIFARE Classic tag.
            // See: http://www.nxp.com/documents/application_note/AN10833.pdf
            // (Table 5 and 6)
            // 0x28 is for some emulated tags.
            /*
            NfcA nfca = NfcA.get(tag);
            byte[] atqa = nfca.getAtqa();
            if (atqa[1] == 0 &&
                    (atqa[0] == 4 || atqa[0] == (byte)0x44 ||
                     atqa[0] == 2 || atqa[0] == (byte)0x42)) {
                // ATQA says it is most likely a MIFARE Classic tag.
                byte sak = (byte)nfca.getSak();
                if (sak == 8 || sak == 9 || sak == (byte)0x18 ||
                                            sak == (byte)0x88 ||
                                            sak == (byte)0x28) {
                    // SAK says it is most likely a MIFARE Classic tag.
                    // --> Device does not support MIFARE Classic.
                    return -1;
                }
            }
            // Nope, it's not the device (most likely).
            // The tag does not support MIFARE Classic.
            return -2;
            */
        }
    }

    public static SpannableString colorString(String data, int color) {
        SpannableString ret = new SpannableString(data);
        ret.setSpan(new ForegroundColorSpan(color),
                0, data.length(), 0);
        return ret;
    }

    public static int dpToPx(int dp) {
        return (int) (dp * mAppContext.getResources().getDisplayMetrics().density + 0.5f);
    }

    public static boolean isExternalStorageWritableErrorToast(
            Context context) {
        if (!isExternalStorageMounted()) {
            Toast.makeText(context, R.string.info_no_external_storage,
                    Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }
    public static File getFileFromStorage(String relativePath,
                                          boolean forceExternal) {
        File file;
        boolean isUseInternalStorage = PreferencesUtil. getPreferences().getBoolean(
                Constants.UseInternalStorage, false);
        if (!forceExternal && isUseInternalStorage) {
            // Use internal storage.
            file = new File(mAppContext.getFilesDir() + relativePath);
        } else {
            // Use external storage (default).
            file = new File(Environment.getExternalStorageDirectory() +
                    relativePath);
        }
        return file;
    }

    public static File getFileFromStorage(String relativePath) {
        return getFileFromStorage(relativePath, false);
    }

    public static MCReader checkForTagAndCreateReader(Context context) {
        MCReader reader;
        boolean tagLost = false;
        // Check for tag.
        if (NFCApp.getTag() != null && (reader = MCReader.get(NFCApp.getTag())) != null) {
            try {
                reader.connect();
            } catch (Exception e) {
                tagLost = true;
            }
            if (!tagLost && !reader.isConnected()) {
                reader.close();
                tagLost = true;
            }
            if (!tagLost) {
                return reader;
            }
        }

        // Error. The tag is gone.
        Toast.makeText(context, R.string.info_no_tag_found,
                Toast.LENGTH_LONG).show();
        return null;
    }

}
